import { useEffect, useMemo, useState } from 'react'; import { Alert, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, TextInput, View, } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import { ResizeMode, Video } from 'expo-av'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { ThemedButton } from '@/components/themed-button'; import { IconButton } from '@/components/icon-button'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { ZoomImageModal } from '@/components/zoom-image-modal'; import { Colors } from '@/constants/theme'; import { useColorScheme } from '@/hooks/use-color-scheme'; import { useTranslation } from '@/localization/i18n'; import { dbPromise, initCoreTables } from '@/services/db'; type FieldRow = { id: number; name: string | null; area_ha: number | null; notes: string | null; photo_uri: string | null; created_at: string | null; updated_at: string | null; }; type FieldMediaRow = { uri: string | null; }; export default function FieldDetailScreen() { const { t } = useTranslation(); const router = useRouter(); const { id } = useLocalSearchParams<{ id?: string | string[] }>(); const fieldId = Number(Array.isArray(id) ? id[0] : id); const theme = useColorScheme() ?? 'light'; const palette = Colors[theme]; const [loading, setLoading] = useState(true); const [status, setStatus] = useState(''); const [saving, setSaving] = useState(false); const [showSaved, setShowSaved] = useState(false); const [name, setName] = useState(''); const [areaHa, setAreaHa] = useState(''); const [notes, setNotes] = useState(''); const [mediaUris, setMediaUris] = useState([]); const [activeUri, setActiveUri] = useState(null); const [errors, setErrors] = useState<{ name?: string; area?: string }>({}); const [zoomUri, setZoomUri] = useState(null); useEffect(() => { let isActive = true; async function loadField() { if (!Number.isFinite(fieldId)) { setStatus(t('fields.empty')); setLoading(false); return; } try { await initCoreTables(); const db = await dbPromise; const rows = await db.getAllAsync( 'SELECT id, name, area_ha, notes, photo_uri, created_at, updated_at FROM fields WHERE id = ? LIMIT 1;', fieldId ); if (!isActive) return; const field = rows[0]; if (!field) { setStatus(t('fields.empty')); setLoading(false); return; } setName(field.name ?? ''); setAreaHa(field.area_ha !== null ? String(field.area_ha) : ''); setNotes(field.notes ?? ''); const mediaRows = await db.getAllAsync( 'SELECT uri FROM field_media WHERE field_id = ? ORDER BY created_at ASC;', fieldId ); const media = uniqueMediaUris([ ...(mediaRows.map((row) => row.uri).filter(Boolean) as string[]), ...(normalizeMediaUri(field.photo_uri) ? [normalizeMediaUri(field.photo_uri) as string] : []), ]); setMediaUris(media); setActiveUri(media[0] ?? normalizeMediaUri(field.photo_uri)); setStatus(''); } catch (error) { if (isActive) setStatus(`Error: ${String(error)}`); } finally { if (isActive) setLoading(false); } } loadField(); return () => { isActive = false; }; }, [fieldId, t]); const inputStyle = [ styles.input, { borderColor: palette.border, backgroundColor: palette.input, color: palette.text, }, ]; async function handleUpdate() { if (!Number.isFinite(fieldId)) return; const trimmedName = name.trim(); const area = areaHa.trim() ? Number(areaHa) : null; const nextErrors: { name?: string; area?: string } = {}; if (!trimmedName) { nextErrors.name = t('fields.nameRequired'); } if (areaHa.trim() && !Number.isFinite(area)) { nextErrors.area = t('fields.areaInvalid'); } setErrors(nextErrors); if (Object.keys(nextErrors).length > 0) { return; } try { setSaving(true); const db = await dbPromise; const now = new Date().toISOString(); const primaryUri = mediaUris[0] ?? normalizeMediaUri(activeUri); await db.runAsync( 'UPDATE fields SET name = ?, area_ha = ?, notes = ?, photo_uri = ?, updated_at = ? WHERE id = ?;', trimmedName, area, notes.trim() || null, primaryUri ?? null, now, fieldId ); await db.runAsync('DELETE FROM field_media WHERE field_id = ?;', fieldId); const mediaToInsert = uniqueMediaUris([ ...mediaUris, ...(normalizeMediaUri(activeUri) ? [normalizeMediaUri(activeUri) as string] : []), ]); for (const uri of mediaToInsert) { await db.runAsync( 'INSERT INTO field_media (field_id, uri, media_type, created_at) VALUES (?, ?, ?, ?);', fieldId, uri, isVideoUri(uri) ? 'video' : 'image', now ); } setStatus(t('fields.saved')); setShowSaved(true); setTimeout(() => { setShowSaved(false); setStatus(''); }, 1800); } catch (error) { setStatus(`Error: ${String(error)}`); } finally { setSaving(false); } } function confirmDelete() { Alert.alert( t('fields.deleteTitle'), t('fields.deleteMessage'), [ { text: t('fields.cancel'), style: 'cancel' }, { text: t('fields.delete'), style: 'destructive', onPress: async () => { const db = await dbPromise; await db.runAsync('DELETE FROM field_media WHERE field_id = ?;', fieldId); await db.runAsync('DELETE FROM fields WHERE id = ?;', fieldId); router.back(); }, }, ] ); } const previewUri = useMemo(() => normalizeMediaUri(activeUri), [activeUri]); return ( {t('fields.edit')} {status && !showSaved ? {status} : null} {t('fields.name')} * { setName(value); if (errors.name) setErrors((prev) => ({ ...prev, name: undefined })); }} placeholder={t('fields.name')} placeholderTextColor={palette.placeholder} style={inputStyle} /> {errors.name ? {errors.name} : null} {t('fields.area')} { setAreaHa(value); if (errors.area) setErrors((prev) => ({ ...prev, area: undefined })); }} placeholder={t('fields.areaPlaceholder')} placeholderTextColor={palette.placeholder} style={inputStyle} keyboardType="decimal-pad" /> {errors.area ? {errors.area} : null} {t('fields.notes')} {t('fields.addMedia')} {previewUri ? ( isVideoUri(previewUri) ? ( setZoomUri(null)} /> ); } async function handlePickMedia(onAdd: (uris: string[]) => void) { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: getMediaTypes(), quality: 1, allowsMultipleSelection: true, selectionLimit: 0, }); if (result.canceled) return; const uris = (result.assets ?? []).map((asset) => asset.uri).filter(Boolean) as string[]; if (uris.length === 0) return; onAdd(uris); } async function handleTakeMedia(onAdd: (uri: string | null) => void) { const permission = await ImagePicker.requestCameraPermissionsAsync(); if (!permission.granted) { return; } const result = await ImagePicker.launchCameraAsync({ mediaTypes: getMediaTypes(), quality: 1, }); if (result.canceled) return; const asset = result.assets[0]; onAdd(asset.uri); } function getMediaTypes() { const mediaType = (ImagePicker as { MediaType?: { Image?: unknown; Images?: unknown; Video?: unknown; Videos?: unknown }; }).MediaType; const imageType = mediaType?.Image ?? mediaType?.Images; const videoType = mediaType?.Video ?? mediaType?.Videos; if (imageType && videoType) { return [imageType, videoType]; } return imageType ?? videoType ?? ['images', 'videos']; } function isVideoUri(uri: string) { return /\.(mp4|mov|m4v|webm|avi|mkv)(\?.*)?$/i.test(uri); } function normalizeMediaUri(uri?: string | null) { if (typeof uri !== 'string') return null; const trimmed = uri.trim(); return trimmed ? trimmed : null; } function uniqueMediaUris(uris: string[]) { const seen = new Set(); const result: string[] = []; for (const uri of uris) { if (!uri || seen.has(uri)) continue; seen.add(uri); result.push(uri); } return result; } const styles = StyleSheet.create({ container: { flex: 1, }, keyboardAvoid: { flex: 1, }, content: { padding: 16, gap: 10, paddingBottom: 40, }, input: { borderRadius: 10, borderWidth: 1, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, }, requiredMark: { color: '#C0392B', fontWeight: '700', }, errorText: { color: '#C0392B', fontSize: 12, }, mediaPreview: { width: '100%', height: 220, borderRadius: 12, backgroundColor: '#1C1C1C', }, photoRow: { flexDirection: 'row', gap: 8, }, actions: { marginTop: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 10, }, photoPlaceholder: { opacity: 0.6, }, mediaStrip: { marginTop: 6, }, mediaChip: { width: 72, height: 72, borderRadius: 10, marginRight: 8, overflow: 'hidden', backgroundColor: '#E6E1D4', alignItems: 'center', justifyContent: 'center', }, mediaThumb: { width: '100%', height: '100%', }, videoThumb: { width: '100%', height: '100%', backgroundColor: '#1C1C1C', alignItems: 'center', justifyContent: 'center', }, videoThumbText: { color: '#FFFFFF', fontSize: 18, fontWeight: '700', }, mediaRemove: { position: 'absolute', top: 4, right: 4, width: 18, height: 18, borderRadius: 9, backgroundColor: 'rgba(0,0,0,0.6)', alignItems: 'center', justifyContent: 'center', }, mediaRemoveText: { color: '#FFFFFF', fontSize: 12, lineHeight: 14, fontWeight: '700', }, updateGroup: { flexDirection: 'row', alignItems: 'center', gap: 8, }, inlineToastText: { fontWeight: '700', fontSize: 12, }, });